package com.tianji.learning.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.api.client.course.CatalogueClient;
import com.tianji.api.client.course.CourseClient;
import com.tianji.api.dto.IdAndNumDTO;
import com.tianji.api.dto.course.CataSimpleInfoDTO;
import com.tianji.api.dto.course.CourseFullInfoDTO;
import com.tianji.api.dto.course.CourseSimpleInfoDTO;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.domain.query.PageQuery;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.utils.*;
import com.tianji.learning.domain.po.LearningLesson;
import com.tianji.learning.domain.po.LearningRecord;
import com.tianji.learning.domain.vo.LearningLessonVO;
import com.tianji.learning.domain.vo.LearningPlanPageVO;
import com.tianji.learning.domain.vo.LearningPlanVO;
import com.tianji.learning.enums.LessonStatus;
import com.tianji.learning.enums.PlanStatus;
import com.tianji.learning.mapper.LearningLessonMapper;
import com.tianji.learning.mapper.LearningRecordMapper;
import com.tianji.learning.service.ILearningLessonService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * <p>
 * 学生课程表 服务实现类
 * </p>
 *
 * @author zr
 * @since 2024-07-10
 */

@Service
@RequiredArgsConstructor
@Slf4j
public class LearningLessonServiceImpl extends ServiceImpl<LearningLessonMapper, LearningLesson> implements ILearningLessonService {

    private final CourseClient courseClient;
    private final CatalogueClient catalogueClient;
    private final LearningRecordMapper recordMapper;

    /**
     * 为用户添加课程。
     * <p>
     * 本方法用于将指定的课程添加到指定用户的课程列表中。这用于用户报名/购买新课程，
     * 课程的添加是通过课程的ID列表来进行的，这允许一次性添加多个课程。
     *
     * @param userId    用户的ID。这个参数指定哪个用户将获得新的课程。
     * @param courseIds 课程的ID列表。这个列表中的每个ID代表一个将被添加到用户课程列表中的课程。
     */
    @Override
    @Transactional
    public void addUserLessons(Long userId, List<Long> courseIds) {
        // 1.查询课程有效期
        List<CourseSimpleInfoDTO> cInfoList = courseClient.getSimpleInfoList(courseIds);
        if (CollUtils.isEmpty(cInfoList)) {
            // 课程不存在，无法添加
            log.error("课程信息不存在，无法添加到课表");
            return;
        }
        // 2.循环遍历，处理LearningLesson数据
        List<LearningLesson> list = new ArrayList<>(cInfoList.size());
        for (CourseSimpleInfoDTO cInfo : cInfoList) {
            LearningLesson lesson = new LearningLesson();
            // 2.1.获取过期时间
            Integer validDuration = cInfo.getValidDuration();
            if (validDuration != null && validDuration > 0) {
                LocalDateTime now = LocalDateTime.now();
                lesson.setCreateTime(now);
                lesson.setExpireTime(now.plusMonths(validDuration));
            }
            // 2.2.填充userId和courseId
            lesson.setUserId(userId);
            lesson.setCourseId(cInfo.getId());
            list.add(lesson);
        }
        // 3.批量新增
        saveBatch(list);
    }

    /**
     * 查询我的课程列表
     *
     * @param query 分页查询条件
     * @return 分页结果，包含课程列表信息
     */
    @Override
    public PageDTO<LearningLessonVO> queryMyLessons(PageQuery query) {
        // 1.获取当前登录用户
        Long userId = UserContext.getUser();
        // 2.分页查询
        // select * from learning_lesson where user_id = #{userId} order by latest_learn_time limit 0, 5
        Page<LearningLesson> page = lambdaQuery().eq(LearningLesson::getUserId, userId) // where user_id = #{userId}
                .page(query.toMpPage("latest_learn_time", false));
        List<LearningLesson> records = page.getRecords();
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        // 3.查询课程信息
        Map<Long, CourseSimpleInfoDTO> cMap = queryCourseSimpleInfoList(records);

        // 4.封装VO返回
        List<LearningLessonVO> list = new ArrayList<>(records.size());
        // 4.1.循环遍历，把LearningLesson转为VO
        for (LearningLesson r : records) {
            // 4.2.拷贝基础属性到vo
            LearningLessonVO vo = BeanUtils.copyBean(r, LearningLessonVO.class);
            // 4.3.获取课程信息，填充到vo
            CourseSimpleInfoDTO cInfo = cMap.get(r.getCourseId());
            vo.setCourseName(cInfo.getName());
            vo.setCourseCoverUrl(cInfo.getCoverUrl());
            vo.setSections(cInfo.getSectionNum());
            list.add(vo);
        }
        return PageDTO.of(page, list);
    }

    /**
     * 查询当前正在进行的课程。
     * <p>
     * 本方法用于获取用户当前正在进行的学习课程的信息。
     *
     * @return LearningLessonVO 返回一个LearningLessonVO对象，其中包含了当前课程的信息。
     */
    @Override
    public LearningLessonVO queryMyCurrentLesson() {
        // 1.获取当前登录的用户
        Long userId = UserContext.getUser();
        // 2.查询正在学习的课程 select * from xx where user_id = #{userId} AND status = 1 order by latest_learn_time limit 1
        LearningLesson lesson = lambdaQuery().eq(LearningLesson::getUserId, userId).eq(LearningLesson::getStatus,
                LessonStatus.LEARNING.getValue()).orderByDesc(LearningLesson::getLatestLearnTime).last("limit 1").one();
        if (lesson == null) {
            return null;
        }
        // 3.拷贝PO基础属性到VO
        LearningLessonVO vo = BeanUtils.copyBean(lesson, LearningLessonVO.class);
        // 4.查询课程信息
        CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
        if (cInfo == null) {
            throw new BadRequestException("课程不存在");
        }
        vo.setCourseName(cInfo.getName());
        vo.setCourseCoverUrl(cInfo.getCoverUrl());
        vo.setSections(cInfo.getSectionNum());
        // 5.统计课表中的课程数量 select count(1) from xxx where user_id = #{userId}
        Integer courseAmount = lambdaQuery().eq(LearningLesson::getUserId, userId).count();
        vo.setCourseAmount(courseAmount);
        // 6.查询小节信息
        List<CataSimpleInfoDTO> cataInfos =
                catalogueClient.batchQueryCatalogue(CollUtils.singletonList(lesson.getLatestSectionId()));
        if (!CollUtils.isEmpty(cataInfos)) {
            CataSimpleInfoDTO cataInfo = cataInfos.get(0);
            vo.setLatestSectionName(cataInfo.getName());
            vo.setLatestSectionIndex(cataInfo.getCIndex());
        }
        return vo;
    }

    /**
     * 根据课程ID查询指定课程
     * <p>
     * 学习视图对象（LearningLessonVO）封装了关于课程学习资料的详细信息，供获取课程信息。
     *
     * @param courseId 课程的唯一标识ID。此ID用于在数据库中定位特定课程的信息。
     * @return LearningLessonVO 包含查询到的课程学习资料信息。如果找不到匹配的资料，则返回null。
     */
    @Override
    public LearningLessonVO queryLessonByCourseId(Long courseId) {
        // 1.获取当前登录用户
        Long userId = UserContext.getUser();
        // 2.查询课程信息 select * from xx where user_id = #{userId} AND course_id = #{courseId}
        LearningLesson lesson = getOne(buildUserIdAndCourseIdWrapper(userId, courseId));
        if (lesson == null) {
            return null;
        }
        // 3.处理VO
        return BeanUtils.copyBean(lesson, LearningLessonVO.class);
    }

    /**
     * 删除课表中课程。
     * <p>
     * 用户直接删除已失效的课程
     * 用户退款后触发课表自动删除
     *
     * @param userId   需要被删除的用户的ID。这是用户在系统中的唯一标识符。
     * @param courseId 课程的ID。这是课程在系统中的唯一标识符。
     */
    @Override
    public void deleteCourseFromLesson(Long userId, Long courseId) {
        // 1.获取当前登录用户
        if (userId == null) {
            userId = UserContext.getUser();
        }
        // 2.删除课程
        remove(buildUserIdAndCourseIdWrapper(userId, courseId));
    }

    /**
     * 根据课程ID统计学习该课程的学员数量。
     * <p>
     * 本方法通过查询数据库或相关数据存储系统，统计指定课程ID对应课程的学习者数量。
     * 课程ID作为参数传入，方法返回学习该课程的学员数量。
     *
     * @param courseId 课程的唯一标识ID，用于指定查询的课程。
     * @return 返回学习该课程的学员数量，如果课程不存在或没有学员学习，则返回null或0。
     */
    @Override
    public Integer countLearningLessonByCourse(Long courseId) {
        // select count(1) from xx where course_id = #{cc} AND status in (0, 1, 2)
        return lambdaQuery()
                .eq(LearningLesson::getCourseId, courseId)
                .in(LearningLesson::getStatus,
                        LessonStatus.NOT_BEGIN.getValue(),
                        LessonStatus.LEARNING.getValue(),
                        LessonStatus.FINISHED.getValue())
                .count();
    }

    /**
     * 检查课程是否有效。
     * <p>
     * 本方法通过课程ID来判断一门课程是否有效。课程的有效性可能取决于多个因素，
     * 如课程是否已被删除、课程是否已过期等。
     *
     * @param courseId 课程的唯一标识ID。这个ID用于在数据库查找对应的课程信息。
     * @return 如果课程有效，则返回课程ID；如果课程无效，则返回null。
     */
    @Override
    public Long isLessonValid(Long courseId) {
        // 1.获取登录用户
        Long userId = UserContext.getUser();
        if (userId == null) {
            return null;
        }
        // 2.查询课程
        LearningLesson lesson = getOne(buildUserIdAndCourseIdWrapper(userId, courseId));
        if (lesson == null) {
            return null;
        }
        // 3.该课程非永久有效且已经过期
        if(lesson.getExpireTime() != null && LocalDateTime.now().isAfter(lesson.getExpireTime())){
            return  null;
        }
        return lesson.getId();
    }

    @Override
    public LearningLesson queryByUserAndCourseId(Long userId, Long courseId) {
        return getOne(buildUserIdAndCourseIdWrapper(userId, courseId));
    }

    /**
     * 创建学习计划
     * <p>
     * 本方法用于根据指定的课程ID和学习频率创建一个新的学习计划。
     * 通过指定课程ID，我们可以确定学习计划针对的具体课程。而学习频率则用于确定学习任务的重复周期。
     *
     * @param courseId 课程ID，用于指定学习计划针对的课程。
     * @param freq     学习频率，表示学习任务的重复周期，例如每周一次。
     */
    @Override
    public void createLearningPlan(Long courseId, Integer freq) {
        // 1.获取当前登录的用户
        Long userId = UserContext.getUser();
        // 2.查询课表中的指定课程有关的数据
        LearningLesson lesson = queryByUserAndCourseId(userId, courseId);
        AssertUtils.isNotNull(lesson, "课程信息不存在！");
        // 3.修改数据
        LearningLesson l = new LearningLesson();
        l.setId(lesson.getId());
        l.setWeekFreq(freq);
        if(lesson.getPlanStatus() == PlanStatus.NO_PLAN) {
            l.setPlanStatus(PlanStatus.PLAN_RUNNING);
        }
        updateById(l);
    }

    /**
     * 查询我的学习计划
     * <p>
     * 通过PageQuery参数查询用户的学习计划，返回LearningPlanPageVO对象，其中包含查询结果和分页信息。
     *
     * @param query 查询条件，包含分页和过滤条件等。
     * @return LearningPlanPageVO 对象，包含查询结果和分页信息。
     */
    @Override
    public LearningPlanPageVO queryMyPlans(PageQuery query) {
        LearningPlanPageVO planPageVO = new LearningPlanPageVO();
        // 1.获取当前登录用户
        Long userId = UserContext.getUser();
        // 2.获取本周起始时间
        LocalDate now = LocalDate.now();
        LocalDateTime begin = DateUtils.getWeekBeginTime(now);
        LocalDateTime end = DateUtils.getWeekEndTime(now);
        // 3.查询总的统计数据
        // 3.1.本周总的已学习小节数量
        Integer weekFinished = recordMapper.selectCount(new LambdaQueryWrapper<LearningRecord>()
                .eq(LearningRecord::getUserId, userId)
                .eq(LearningRecord::getFinished, true)
                .gt(LearningRecord::getFinishTime, begin)
                .lt(LearningRecord::getFinishTime, end));
        planPageVO.setWeekFinished(weekFinished);
        // 3.2.本周总的计划学习小节数量
        QueryWrapper<LearningLesson> wrapper = new QueryWrapper<>();
        wrapper.select("SUM(week_freq) AS plansTotal");
        wrapper.eq("user_id", userId);
        wrapper.in("status", LessonStatus.NOT_BEGIN, LessonStatus.LEARNING);
        wrapper.eq("plan_status", PlanStatus.PLAN_RUNNING);
        Map<String, Object> map = getMap(wrapper);
        if(map != null && map.get("plansTotal") != null){
            //得到结果
            Object plansTotal = map.get("plansTotal");
            //SUM类型默认为BigDecimal，所以需要转换为int
            Integer plans = Integer.valueOf(plansTotal.toString());
            planPageVO.setWeekTotalPlan(plans);
        }else{
            planPageVO.setWeekTotalPlan(0);
        }
        // TODO 3.3.本周学习积分
        // 4.查询分页数据
        // 4.1.分页查询课表信息以及学习计划信息
        Page<LearningLesson> p = lambdaQuery()
                .eq(LearningLesson::getUserId, userId)
                .eq(LearningLesson::getPlanStatus, PlanStatus.PLAN_RUNNING)
                .in(LearningLesson::getStatus, LessonStatus.NOT_BEGIN, LessonStatus.LEARNING)
                .page(query.toMpPage("latest_learn_time", false));
        List<LearningLesson> records = p.getRecords();
        // 如果没有计划的课程返回一个空分页
        if(CollUtils.isEmpty(records)){
            planPageVO.setTotal(0L);
            planPageVO.setPages(0L);
            planPageVO.setList(CollUtils.emptyList());
            return planPageVO;
        }
        // 4.2.查询课表对应的课程信息
        Map<Long, CourseSimpleInfoDTO> cMap = queryCourseSimpleInfoList(records);
        // 4.3.统计每一个课程本周已学习小节数量 sql做法
        List<IdAndNumDTO> list = recordMapper.countLearnedSections(userId, begin, end);
        Map<Long, Integer> countMap = IdAndNumDTO.toMap(list);
        //先查出已学小节的集合，然后遍历集合输出map<课表id, 已学小节数量>
/*        Map<Long, Long> weekLearnedSections = recordMapper.selectList(
                new LambdaQueryWrapper<LearningRecord>()
                        .eq(LearningRecord::getUserId, 2)
                        .eq(LearningRecord::getFinished, 1)
                        .gt(LearningRecord::getFinishTime, begin)
                        .lt(LearningRecord::getFinishTime, end)
                //对课表分组就是对课程分组，groupingBy分组时调用counting函数即可对分组中的数量进行统计，统计的结果就是已学小节数
        ).stream().collect(Collectors.groupingBy(LearningRecord::getLessonId, Collectors.counting()));*/
        // 4.4.组装数据VO
        ArrayList<LearningPlanVO> voList = new ArrayList<>(records.size());
        for (LearningLesson r : records) {
            // 4.4.1.拷贝基础属性到vo
            LearningPlanVO vo = BeanUtils.copyBean(r, LearningPlanVO.class);
            // 4.4.2.填充课程详细信息
            CourseSimpleInfoDTO cInfo = cMap.get(r.getCourseId());
            if (cInfo != null) {
                vo.setCourseName(cInfo.getName());
                vo.setSections(cInfo.getSectionNum());
            }
            // 4.4.3.每个课程的本周已学习小节数量
            vo.setWeekLearnedSections(countMap.getOrDefault(r.getId(), 0));
            voList.add(vo);
        }
        return planPageVO.pageInfo(p.getTotal(), p.getPages(), voList);
    }

    /**
     * 构建查询Wrapper，用于筛选指定用户和课程的信息。
     *
     * @param userId   用户ID，用于查询该用户的信息。
     * @param courseId 课程ID，用于查询该课程的信息。
     * @return 返回一个LambdaQueryWrapper实例，已设置好查询条件。
     */
    private LambdaQueryWrapper<LearningLesson> buildUserIdAndCourseIdWrapper(Long userId, Long courseId) {
        return new QueryWrapper<LearningLesson>().lambda().eq(LearningLesson::getUserId, userId).eq(LearningLesson::getCourseId, courseId);
    }


    /**
     * 根据课程ID列表查询课程的简单信息。
     * <p>
     * 此方法通过调用课程服务，根据一系列课程ID获取这些课程的简单信息。
     * 如果找不到对应的课程信息，则抛出BadRequestException异常。
     *
     * @param records 包含课程ID的列表，这些ID用于查询课程信息。
     * @return 一个映射，其中键是课程ID，值是对应的课程简单信息DTO对象。
     * 这个映射用于快速根据课程ID查找课程信息，提高后续处理的效率。
     */
    private Map<Long, CourseSimpleInfoDTO> queryCourseSimpleInfoList(List<LearningLesson> records) {
        // 3.1.获取课程id
        Set<Long> cIds = records.stream().map(LearningLesson::getCourseId).collect(Collectors.toSet());
        // 3.2.查询课程信息
        List<CourseSimpleInfoDTO> cInfoList = courseClient.getSimpleInfoList(cIds);
        if (CollUtils.isEmpty(cInfoList)) {
            // 课程不存在，无法添加
            throw new BadRequestException("课程信息不存在！");
        }
        // 3.3.把课程集合处理成Map，key是courseId，值是course本身
        return cInfoList.stream().collect(Collectors.toMap(CourseSimpleInfoDTO::getId, c -> c));
    }
}
